#!/bin/bash

# Author:	Copyright 2008,2009 Tom Kacperski
# Site:		http://www.microdevsys.com
# Wiki:		http://www.microdevsys.com/WordPress/2009/01/08/securing-your-website-apache-httpd-tomcat-port-80-from-malicious-scans/
# Date:		Jan 18 2009
# Under: 	GPL (http://www.gnu.org/licenses/gpl.html)
# Name: 	Intrusion Monitoring and Resolution script.

#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.

#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.

#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.


# ------------------------------------------------------------------------------------------------------------------
#
#	CONSTANT BLOCK
#
#
#
# ------------------------------------------------------------------------------------------------------------------

scriptname=$(basename $0);
ssince=$(date +%s);
scriptpid=$$;
ipfile="imrIPSet.txt";
cutofft=86400;		# 1 Day duration.
cutoffl=604800;		# 7 Day duration.
occthreshold=10;	# Number of times the same IP has been blocked in the past.
notifycutoff=300;
scanfile="/var/log/httpd/access_log";
archive="imrArchive.txt";
logfile="";
blockip="";
tmppath="/tmp/imr.sh";
tmplog="imr.sh.log";
serverinit="";
shost="localhost";

FALSE=1;
TRUE=0;

enablelogs=$TRUE;

export INTRUDER_SH_PID="";
export INTRUDER_SH_PORT=11235;
mhash="Jan:01 Feb:02 Mar:03 Apr:04 May:05 Jun:06 Jul:07 Aug:08 Sep:09 Oct:10 Nov:11 Dec:12";

# ------------------------------------------------------------------------------------------------------------------
#
#	FUNCTION BLOCK
#
#
#
# ------------------------------------------------------------------------------------------------------------------

# This is an implementation of a bash hash table / array.  This function returns the value associated with a key provided as one of the parameters.
function hashv {
        hkey="";
        mh="";
        if [[ $2 != "" ]]; then hkey=$2; else echo ""; return 0; fi
        if [[ $1 != "" ]]; then mh=$1; else echo ""; return 0; fi

        echo $mh|sed -ne "s/.*\([ \t]*\)\($hkey\):\([^ \t]*\?\)\([ \t]*\).*/\3/gpi";
}

# This is an implementation of a bash hash table / array.  This function returns the key associated with a value provided as one of the parameters.
function hashk {
        hvalue="";
        mh="";
        if [[ $2 != "" ]]; then hvalue=$2; else echo ""; return 0; fi
        if [[ $1 != "" ]]; then mh=$1; else echo ""; return 0; fi

        echo $mh|grep "$hvalue"|sed -ne "s/\([^ \t]*\)[:]\($hvalue\)/\|\1|\2\|/i" -e "s/.*\?[|]\(.*\)[|].*[|].*\?/\1/gpi";
}

function lastIncident {
        ipv=$1;

        for adt in $(/bin/cat /var/log/httpd/access_log*|grep $ipv|sed -e "s/.*[\[]\([0-9]\{2\}\)\/\([a-zA-Z]\{3\}\)\/\([0-9]\{4\}\):\([0-9]\{2\}\):\([0-9]\{2\}\):\([0-9]\{2\}\).*/\3-\2-\1_\4:\5:\6/g"); do                                                                                                                                       dmonth=$(echo $adt|awk -F"-" '{ print $2 }');
                nmonth=$(hashv "$mhash" $dmonth);
                lm "[nmonth=|"$nmonth"|\ndmonth=|"$dmonth"|]\n";
                fdt=$(echo -ne $adt|tr '_' ' '|sed -e "s/$dmonth/$nmonth/g");
                lm "|"$fdt"|";
                /bin/date --date="$fdt" +%s;
        done|sort -nr|head -n 1;
}

function isLocal {
	ipa=$1;

	echo $ipa|egrep "^(10|127|192|224|0)\." 1>&2 >/dev/null;		# 0 (TRUE) if local, 1 (FALSE) if not local.
	echo $?;
	return 0;
}

function existsInFile {
	ipa=$1;

	/bin/cat $ipfile|egrep "^$ipa-" 1>&2 >/dev/null;			# 0 (TRUE) if in $ipfile, 1 (FALSE) if not in file.
	echo $?;
	return 0;
}

function inIptables {
	ipa=$1;
	
	/sbin/iptables -nL|grep "$ipa" 1>&2 >/dev/null;				# 0 (TRUE) if in iptables already, 1 (FALSE) if not in iptables.
	echo $?;
	return 0;
}

function isAccessible {
	tfile=$1;

	if [[ -e $tfile && -r $tfile && -w $tfile ]]; then
		echo $TRUE;
	else
		echo $FALSE;
	fi
	return 0;
}

# This function indicates particularly BAD IP's from which repeat or recent attacks occurred from.
function isRepeatRecent {
	ipv=$1;
	ipoccurrences="";
	latestocc="";
	ipoccurrences=$(/bin/cat $archive|awk -F"-" '{ print $1 }'|sort|grep -c $ipv 2>&1);
	latestocc=$(/bin/cat $archive|grep $ipv|sort -nrk2 -t "-"|head -n 1 2>&1|sed -e "s/.*\?[-]\([0-9]\+$\)/\1/g");

	echo -ne ">> latestocc |"$latestocc"| ipoccurrences |"$ipoccurrences"|\n" 2>&1 >> $logfile;
	if [[ $latestocc == "" || $ipoccurrences == "" ]]; then
		echo $FALSE;
		return $FALSE;
	fi

	if (( $ssince - $latestocc < $cutofft )); then		# Intrusion occurred less then 1 day ago, from this same IP.
		if (( ipoccurrences > occthreshold )); then	# If more then X occurrances, return TRUE.
			echo $TRUE;
		else						# else, attacks not originating often from this IP, return FALSE.
			echo $FALSE;
		fi
	else							# If greater then 1 day ago, ie attack from this IP is rare,
		if (( ipoccurrences > occthreshold )); then	# but occurred often earlier.
			echo $TRUE;
		else						# If attack from IP not repeating and not recent, do not block, return FALSE.
			echo $FALSE;
		fi
	fi
}

function wasBlockedEarlier {
	ipv=$1;

	blocktime=$(/bin/cat $ipfile|egrep "^$ipv-"|awk -F"-" '{ print $2 }');
	if (( $ssince - $blocktime > $notifycutoff )); then
		echo $FALSE;
	else
		echo $TRUE;
	fi
	return 0;
}

function plm {
	msg=$1;

	if [[ ! $msg == "" ]] && (( $enablelogs == $TRUE )); then
		echo -ne $(date)" "$msg 2>&1|tee -a $logfile;
	fi
}

function lm {
	msg=$1;

	if [[ ! $msg == "" ]] && (( $enablelogs == $TRUE )); then
		echo -ne $(date)" "$msg >> $logfile;
	fi
}

# RELEASE IP's found in access_log file.
function releaseIntruders {
	ipvalue="";
	iptime="";
	if [[ $(isAccessible $ipfile) == $TRUE ]]; then	
		for ivalue in $(/bin/cat $ipfile); do
			(( i=i+1 ));
			plm "["$i"] "$ivalue"\n";

			ipvalue=$(echo $ivalue|awk -F"-" '{ print $1 }');
			iptime=$(echo $ivalue|awk -F"-" '{ print $2 }');

			isrr=$(isRepeatRecent $ipvalue);
			plm "isRepeatRecent for '$ipvalue' = |"$isrr"|\n";
			plm "Time Difference = |"$(( $ssince - $iptime ))"|\n";
			plm "Greater then cutoffT? |"$(( $ssince - $iptime > $cutofft ))"|\n";
			plm "Greater then cutoffL? |"$(( $ssince - $iptime > $cutoffl ))"|\n";
			plm "|"$ssince"|"$iptime"|"$cutofft"|"$cutoffl"|\n";
			if ( (( $ssince - $iptime > $cutofft )) && [[ $isrr == $FALSE ]] ) ||
			   ( (( $ssince - $iptime > $cutoffl )) && [[ $isrr == $TRUE ]] ); then
				lm "UNBlocking IP:|"$ipvalue"|\n";
			
				# In case the IP has been inserted more then once, repeat until it's removed.
				instan=$(/sbin/iptables -nL|grep $ipvalue|wc -l);
				plm "imr.sh: Removing IP '$ipvalue' from iptables '$instan' times.\n";
				for rep in $(seq 1 $instan); do
					lm "/sbin/iptables($ipvalue) = |";
					/sbin/iptables -D INPUT -i eth0 -s $ipvalue -j REJECT --reject-with icmp-host-unreachable 2>&1 >> $logfile;
					lm "|\n";
				done
			
				plm "Removing IP $ipvalue from $ipfile...\n";
				/bin/sed -i '/'$ipvalue'/d' $ipfile;
			fi
		done
	fi
}

function blockIP {
	ipv="";
	tag="";
	logonly=$FALSE;
	lla="";

	if [[ $1 != "" ]]; then ipv=$1; else return; fi;
	if [[ $2 != "" ]]; then tag=$2; fi;
	if [[ $3 != "" && $3 != $FALSE ]]; then logonly=$3; fi;

	plm "In file? |"$(existsInFile $ipv)"|"$ipv"|"$FALSE"|\n";
	plm "In iptables? |"$(inIptables $ipv)"|"$ipv"|"$FALSE"|\n";

	lla=$(lastIncident $ipv);
	plm "ssince=|"$ssince"| lla=|"$lla"| cutofft=|"$cutofft"|\n";
	if (( $ssince - $lla > $cutofft )); then
		plm "IP $ipv was NOT blocked.  Last occurrance is over one day ago, and therefore not significant to block.\n";
		return 0;
	fi

	if [[  $(existsInFile $ipv) == $FALSE ]]; then
		plm "Adding IP to $ipfile.\n";
		echo $ipv"-"$ssince >> $ipfile;
		if [[ $(inIptables $ipv) == $TRUE ]]; then
			plm "IP is already in IP tables.  Returning TRUE.\n";
			return $TRUE;
		fi
	fi
	if [[ $(existsInFile $ipv) == $TRUE &&  $(inIptables $ipv) == $FALSE ]]; then
		plm "IP existed in file but not in iptables.  Defaulting to unblocking it.\n";
		/sbin/iptables -D INPUT -i eth0 -s $ipv -j REJECT --reject-with icmp-host-unreachable 2>&1 >/dev/null;
		/bin/sed -i '/'$ipv'/d' $ipfile;
	fi
	echo $ipv"-"$ssince >> $archive;

	if (( logonly == TRUE )); then
		plm "imr.sh$tag: Logging IP:|"$ipv"|\n";
	else 
		plm "imr.sh$tag: Blocking IP:|"$ipv"|\n";
		if [[ $(inIptables $ipv) == $FALSE ]]; then
			/sbin/iptables -A INPUT -i eth0 -s $ipv -j REJECT --reject-with icmp-host-unreachable;
		else
			plm "NOTE: Nothing was blocked.  IP $ipv is already in iptables.  Use 'iptables -nL' to check.\n";
		fi
	fi
}

# BLOCK IP's found in access_log file.
function blockIntruders {
	# echo "[s][blockIntruders] |$scanfile|";
	rcheck=0;

	for ivalue in $(/bin/cat $scanfile|awk '{ print $1 }'|sort|uniq); do
		(( i=i+1 ));
		if [[ $(isLocal $ivalue) == $FALSE && $(existsInFile $ivalue) == $FALSE && $(inIptables $ivalue) == $FALSE  ]]; then
			plm "Blocking...\n";
			blockIP $ivalue " : (Blocking the IP) ";
		elif [[ $(existsInFile $ivalue) == $TRUE && $(inIptables $ivalue) == $TRUE && $(wasBlockedEarlier $ivalue) == $TRUE ]]; then
			plm "Logging...\n";
			blockIP $ivalue " : (Already blocked in iptables.  Logging activity ONLY) " $TRUE;	# LOG entry only.
		else
			if [[ $(isLocal $ivalue) == $FALSE ]]; then
				plm "Checking IP: $ivalue\n";
				if [[ $(existsInFile $ivalue) == $FALSE || $(inIptables $ivalue) == $FALSE ]]; then
					blockIP $ivalue " : (Resolving inconsistency) ";
				fi
			fi
		fi
	done

	ivalue="";
	for ivalue in $(/bin/cat $ipfile|awk -F"-" '{ print $1 }'|sort|uniq); do
		if [[ $(isLocal $ivalue) == $FALSE ]]; then
			plm "Checking IP: $ivalue\n";
			if [[ $(existsInFile $ivalue) == $FALSE || $(inIptables $ivalue) == $FALSE ]]; then
				blockIP $ivalue " : (Resolving inconsistency) ";
			fi
		fi
	done
	# echo "[e][blockIntruders]";
}

# Check if we were called from within an httpd daemon (spec. ModSecurity) to block an IP.  Get the IP from REMOTE_ADDR as it should be set in this case.
function isModSecurityInvoked {
	parentpid=$scriptpid;
	fromapache=$FALSE;
	# echo "|"$scriptpid"|"$parentpid"|"$fromapache"|";
	plm "isModSecurityInvoked: Checking if invocation came from ModSecurity.\n";
	while ((1)); do
		if (( parentpid <= 1 || parentpid >= 4194304 )); then
			break;
		fi
		plm "NOTE: Old parentpid = |"$parentpid"|\n";
		parentpid=$(ps -o ppid --no-headers $parentpid);
		if [[ $parentpid != "" ]]; then
			errormsg=$(/bin/ps -o pid -o command= $parentpid|grep "/usr/sbin/httpd"|grep -v grep 2>&1);
			retval=$?
		else
			pm "ERROR: parentpid came back invalid after trying to retrieve it's parent.  parentpid =|"$parentpid"|\n";
			break;
		fi
		plm "FALSE = |"$FALSE"| prentpid = |"$parentpid"| retval = |"$retval"| fromapache = |"$fromapache"| errormsg = |"$errormsg"|\n";
		if [[ $retval == 0 && $parentpid != "1" ]]; then
			plm "From Apache HTTPD.\n";
			fromapache=$TRUE;
			break;
		elif [[ ( $parentpid == "1" || $retval == 1 ) && $fromapache == $FALSE && ! $errormsg =~ .*"[Ee][Rr][Rr][Oo][Rr]".* ]]; then
			plm "No results from checking if we were lunched from httpd, therefore we were not.  Leaving check: |"$errormsg"|\n";
			break;	
		elif [[ $retval == 1 && $errormsg =~ .*"[Ee][Rr][Rr][Oo][Rr]".* ]]; then
			plm "ERROR: Issue in /bin/ps command.  Please see above for details.\n";
			exit 0;
		fi
	done

	if [[ $fromapache == $TRUE && ${REMOTE_ADDR} != "" && ${#REMOTE_ADDR} > 0 ]]; then
		blockip=${REMOTE_ADDR};
		plm "imr.sh:  Invoked from ModSecurity. The following IP ("${REMOTE_ADDR}"), if not local type, will be blocked. \n";
		return $TRUE;
	elif [[ $fromapache == $TRUE && ( ${REMOTE_ADDR} == "" || ${#REMOTE_ADDR} == 0 ) ]]; then
		plm "Was invoked from modsecurity however no IP was provided.\n";
		return $FALSE;
	else 
		plm "Was NOT invoked from Mod Security.\n";
		return $FALSE;
	fi

	return $FALSE;
}


function isServerRunning {
	isserverrunning=$(ps axo pid,ppid,user,cmd=|grep "/usr/bin/nc -l $INTRUDER_SH_PORT -k -v"|grep -v "grep /usr/in/nc");

	if [[ $isserverrunning != "" && ${#isserverrunning} > 0 ]]; then
		return $TRUE:
	fi
	return $FALSE;
}

function usage {
	echo "-p|--port	<PORT>			Port on which the server should run on and clients should connect to.";
	echo "-b|--block <IP>				IP to block.";
	echo "-s|--server <status|start|stop>		Either check status, start or stop the server.";
	echo -ne "Written by Tom Kacpersk.  Documentation and details are kept at microdevsys.com/wordpress.  Search string: imr.sh.\n";
}

# --------------------------------------------------------------------
#
#	MAIN
#
#
# --------------------------------------------------------------------
function main {
	cwd=$(pwd);

	/bin/mkdir $tmppath >/dev/null 2>&1;
	/bin/chmod 777 $tmppath >/dev/null 2>&1;
	cd $tmppath;

	# Create and set permissions on script specific log file, if it doesn't exist.
	plm "Result of 'isAccessible $tmppath"/"$tmplog' = |"$(isAccessible $tmppath"/"$tmplog)"|\n";
	if [[ $(isAccessible $tmppath"/"$tmplog) == $FALSE ]]; then
		/bin/touch $tmppath"/"$tmplog 2>&1 >/dev/null;
		if [[ $? != 0 ]]; then
			plm "Failed to create the log file $tmppath/$tmplog. Please check if another file by the same name exists there already owned by another user/group.\n";
			exit 0;
		fi

		/bin/chmod 777 $tmppath"/"$tmplog 2>&1 >/dev/null;
		if [[ $? != 0 ]]; then	
			plm "Failed to set permissions on script log file $tmppath/$tmplog.  Please set 777 permissions manually.  Exiting...\n";
			exit 0;
		fi
	fi

	if [[ $(isAccessible "/var/log/messages") == $TRUE ]]; then
		logfile="/var/log/messages";
	elif [[ $(isAccessible "/var/log/httpd/error_log") == $TRUE ]]; then
		logfile="/var/log/httpd/error_log";
	elif [[ $(isAccessible $tmppath"/"$tmplog) == $TRUE ]]; then
		logfile=$tmppath"/"$tmplog;
	else
		plm "No log files are accessible.  Exiting. \n";
		exit 0;
	fi

	# Get the parameters.
	while [ $# -gt 0 ]; do
		case $1 in
			-h|--help)
				usage;
				exit 0;
				;;
			-p|--port)
				INTRUDER_SH_PORT=$2;
				shift 2;
				;;
			-b|--block)
				blockip=$2;
				shift 2;
				;;
			-s|--server)
				serverinit=$2;
				shift 2;
				;;
			*)
				echo "ERROR: Problems with options: |$1|" 1>&2;
				exit 1
				;;
		esac
	done

	# Check if invoked from ModSecurity module.  If true, default action is assumed to be to block an IP then send IP to running server above.
	isModSecurityInvoked; 	# Call the function.
	modsecurity=$?;		# Get RETURN (return) value from function.
	plm "modsecurity=|$modsecurity|\n";

	if [[ $serverinit =~ .*"[SsTtAaRr]{5}".* ]]; then
		plm "Starting the server.\n";
		ispopen=$(netstat -ant|awk '{ print $4 }'|grep ":$INTRUDER_SH_PORT$" 2>&1 >/dev/null);
		if [[ $? == 1 || ( $? == 0 && ! $isopen =~ .*"[LISTENlisten]{6}".* ) ]]; then
			runecho=$(
				# SERVER START ------------------------------------------
				/usr/bin/nc -l $INTRUDER_SH_PORT -k -v 2>/dev/null|while read line; do 
					if [[ $INTRUDER_SH_PID == "" ]]; then 
						export INTRUDER_SH_PID=$(ps axo pid,ppid,user,cmd=|grep "/usr/bin/nc -l $INTRUDER_SH_PORT -k -v"|grep -v "grep /usr/bin/nc"|awk '{ print $1 }'); 
					fi; 
					if [[ $line == "QUIT" ]]; then 
						echo "Terminating....."; 
						kill -SIGINT $INTRUDER_SH_PID; 
						break; 
					elif [[ $line =~ .*"BLOCK IP:".* ]]; then 
						iptb=$(echo $line|sed -e "s/BLOCK IP:[\t ]\([0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\)/\1/g"); 
						plm ">|"$iptb"|<\n";
						/usr/bin/shared/imr.sh --block $iptb; 
					fi; 
				done; 
				/bin/netstat -pnteea|grep $INTRUDER_SH_PORT 2>&1 >/dev/null;  
				if [[ $? != 0 ]]; then 
					echo "ERROR: Server failed to start. Please check if initiating a test using \"/usr/bin/nc -l $INTRUDER_SH_PORT -k -v\"";
					echo "is successfull and if port $INTRUDER_SH_PORT is open and not used by another application."; 
				fi )&
				# SERVER END --------------------------------------------
		else
			plm "ERROR: Looks like another instance or application is already using port $INTRUDER_SH_PORT.  ";
			plm "Use -p|--port switch to specify another port.\n";
		fi
		serverinit="";
		exit 0;
	elif [[ $serverinit =~ .*"[stopSTOP]{4}".* ]]; then
		plm "Stopping the server.\n";
		isonline=$(netstat -ant|awk '{ print $4 }'|grep ":$INTRUDER_SH_PORT$" 2>&1 >/dev/null);
		if [[ $? == 0 ]]; then
			echo "QUIT"|/usr/bin/nc $shost $INTRUDER_SH_PORT 2>&1 >/dev/null;
			exit 0;
		else
			plm "The server is already stopped.  No action taken.\n";
			exit 0;
		fi
	elif [[ $serverinit =~ .*"[statusSTATUS]{6}".* ]]; then
		plm "Status of service on port $INTRUDER_SH_PORT is: \n";
		isonline=$(/bin/netstat -ant|egrep ":$INTRUDER_SH_PORT[\t ]");
		if [[ $isonline =~ .*"[LISTENlisten]{6}".* ]]; then
			plm "ONLINE and listening.\n";
			exit 0;
		else
			plm "OFFLINE and port is closed.\n";
			exit 0;
		fi
	fi

	if [[ $modsecurity == $FALSE && $blockip != "" && ${#blockip} > 0 && $(isLocal $blockip) == $FALSE ]]; then
		blockIP $blockip;
		blockip="";
	
	# Find server and send request to it as apache can't always see iptables binary anyway.
	elif [[ $modsecurity == $TRUE && $blockip != "" && ${#blockip} > 0 && $(isLocal $blockip) == $FALSE ]]; then	
		ispopen=$(netstat -ant|awk '{ print $4 }'|grep ":$INTRUDER_SH_PORT$" 2>&1 >/dev/null);
		if [[ $? == 1 ]]; then
			plm "ERROR: Attempting to block IP by sending to '/usr/bin/nc $shost $INTRUDER_SH_PORT' but the port is not open.";
			plm "ERROR: Server may be down.  To start server run '$scriptname --server start' or contact your sysadmin to do so.";
		else
			plm "Blocking IP: |"$blockip"|\n";

			# Set IP to the ModSecurity defined variable REMOTE_ADDR, which should hold the IP.  'blockip' is set by isModSecurity function.
			echo "BLOCK IP: "$blockip|/usr/bin/nc $shost $INTRUDER_SH_PORT;
			blockip="";
		fi
	elif [[ $modsecurity == $FALSE ]]; then
		plm "Checking if any IP's can be released.\n";
		releaseIntruders;
		plm "Checking if any IP's need to be blocked.\n";
		blockIntruders;
		plm "Done.\n";
	else
		plm "NOTE: Either the IP to block is local, or you are within modsecurity where iptables is inaccessible.  No actions performed.\n";
	fi
	cd $cwd;
}

main $@;
